﻿package asphalt2.util{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.setTimeout;
	
	import mx.effects.Effect;
	import mx.events.EffectEvent;

	[Event(name="complete", type="flash.events.Event")]
		
	/**
	 * タスクを順次実行するクラス
	 * addTaskで呼び出したいFunctionを渡すと、Taskを１つ追加します。
	 * デフォルトでは、タスクが終了してからtaskToTaskIntervalの間隔をあけて次のタスクを呼び出します。
	 * コンストラクタ引数でtrueを渡すと、直前のタスクが終了したかどうかに関わらずtaskToTaskIntervalの
	 * 間隔でFunctionを呼び出します。
	 * @author c9katayama
	 */
	public class TaskChain extends EventDispatcher{
		/**
		 * 最初のメソッドを呼び出す間隔
		 */
		public var startInterval:uint;
		/**
		 * 処理と処理の間隔
		 */
		public var taskToTaskInterval:uint;
		/**
		 * 並列処理かどうか
		 * trueの場合、直前処理が終了しているかどうかに関わらず
		 * taskToTaskIntervalの間隔で処理を呼び出します。 
		 */
		public var concurrent:Boolean;
		
		//GC対策用のTaskChainMap
		private static var chainMap:Dictionary  = new Dictionary();
		
		//直列実行時にはじめに実行するタスク
		private var first:Task;
		//実行するタスクのリスト
		private var taskList:Array;
		
		//並列実行時のタスクインデックス
		private var taskIndex:uint = 0;
				
		//実行済みフラグ
		private var executed:Boolean = false;
		
		//終了したタスクの数
		private var endTaskNum:uint = 0;
		/**
		 * TaskChainを作成します。 
		 * @param concurrent 並列実行するかどうか　デフォルトはfalse 
		 * @param taskToTaskInterval はじめのタスクが実行されるまでの時間 デフォルト０
		 * @param taskToTaskInterval　タスクとタスクの間の時間　デフォルト０
		 * 
		 **/
		public function TaskChain(concurrent:Boolean=false,startInterval:uint=0,taskToTaskInterval:uint=0){
			taskList = new Array();		
			this.concurrent = concurrent;
			this.startInterval = startInterval;
			this.taskToTaskInterval = taskToTaskInterval;
		}
		/**
		 * TaskChain中に実行するTaskChainを追加します。
		 * @param taskChains 実行するTaskChain(複数可）
		 **/
		public function addTaskChain(...taskChains):TaskChain{
			for each(var taskChain:TaskChain in taskChains){
				if(taskChain==this){
					throw new Error("recursive call not support.");
				}
				var f:Function = function(nextFunction:Function):void{
					taskChain.addEventListener(Event.COMPLETE,
						function():void{
							nextFunction();
						});
					taskChain.execute();
				}
				addAsyncTask(f);
			}
			return this;
		}
		/**
		 * 非同期タスクを追加します。
		 * @param executeFunctions 実行するFunction(複数可）
		 **/
		public function addAsyncTask(...executeFunctions):TaskChain{
			for each(var executeFunction:Function in executeFunctions){
				_addTask(executeFunction,true);
			}
			return this;
		}
		/**
		 * 同期タスクを追加します。
		 * @param executeFunctions 実行するFunction(複数可）
		 **/
		public function addTask(...executeFunctions):TaskChain{
			for each(var executeFunction:Function in executeFunctions){
				_addTask(executeFunction,false);
			}
			return this;
		}
		/**
		 * エフェクトを追加します。
		 * このメソッドでエフェクトを追加すると、Effect.play()を呼び出し、
		 * EffectEvent.EFFECT_ENDで次のタスクを呼び出す処理を行います。
		 * @param effects 実行するエフェクト（複数可）
		 **/
		public function addEffect(...effects):TaskChain{
			for each(var effect:Effect in effects){
				addAsyncTask(_createEffectFunction(effect));
			}
			return this;
		}
		protected function _createEffectFunction(effect:Effect):Function{
			var targetNum:int = effect.targets.length;
			return function(gotoNext:Function):void{
					effect.addEventListener(EffectEvent.EFFECT_END,
						function(e:Event):void{
							targetNum--;
							if(targetNum==0){						
								gotoNext();
							}
						}
					);
					if(!effect.isPlaying){
						effect.play();
					}
			}		
		}
		/**
		 * 戻り値でエフェクトを返すFunctionを追加します。
		 * （例： function():Effect{
		 *      var fade:Face = new Fade(targetComponent);
		 *      fade.alphaFrom=0;
		 *      fade.alphaTo=1;
		 *      return fade;
		 *     }
		 * )
		 * このメソッドでFunctionを追加すると、戻り値のエフェクトのEffect.play()を呼び出し、
		 * EffectEvent.EFFECT_ENDで次のタスクを呼び出す処理を行います。
		 **/
		public function addEffectCreate(...effectCreates):TaskChain{
			for each(var effectCreate:Function in effectCreates){
				addAsyncTask(function(gotoNext:Function):void{
					var o:* = effectCreate();
					if(!(o is Effect)){
						throw new Error("effectCreate return value is not Effect");
					}
					var effect:Effect = o as Effect;
					_createEffectFunction(effect)(gotoNext);
				});
			}
			return this;
		}		
		/**
		 * 次の処理開始を待つタスクを追加します。
		 * @param waitTime:処理待ち時間（1秒=1000)
		 */
		public function addWait(waitTime:int):void{
			addAsyncTask(function(f:Function):void{
				setTimeout(f,waitTime);
			});
		}
		/**
		 * タスクを追加します。
		 * @param executeFunction 実行するFunction
		 * @param async 実行するFunctionが非同期かどうか trueの場合、Functionの引数に次に呼び出すFunctionを宣言し、処理終了時に呼び出す必要があります。　デフォルトfalse 
		 **/
		internal function _addTask(executeFunction:Function,async:Boolean):void{
			var task:Task = new Task();
			task.executeFunction = executeFunction;
			task.taskChain = this;
			task.async = async;
			
			if(!concurrent){
				//直列の場合、タスク同士をチェインする
				if(first==null){
					first = task;
				}else{
					var lastTask:Task = taskList[taskList.length-1];
					lastTask.nextExecuteTask = task;
				}
			}
			task.nextInterval = taskToTaskInterval;
			taskList.push(task);
		}
		/**
		 * タスクの実行
		 */
		public function execute():void{
			if(taskList.length==0){
				return;
			}
			if(executed){
				throw new Error("TaskChain was executed.");
			}else{
				executed = true;
			}
			//GC対象にならないように、staticのマップに格納
			chainMap[this] = this;
			
			if(concurrent){
				setTimeout(parallelExecute,startInterval);
			}else{
				setTimeout(this.first.execute,startInterval);
			}
		}
		public function stop():void{
			for(var i:int = 0;i < taskList.length;i++){
				taskList[i].play = false;
			}
			endTaskChain();
		}
		/**
		 * 並列実行
		 * */
		internal function parallelExecute():void{			
			taskList[taskIndex].execute();
			taskIndex++;
			if(taskIndex < taskList.length){
				setTimeout(parallelExecute,taskToTaskInterval);
			}			 	
		}
		/**
		 * 各タスクが終了したときにコールバック 
		 **/
		internal function endTask():void{
			if(++endTaskNum==taskList.length){
				endTaskChain();
			}
		}
		/**
		 * すべてのタスクが終了したときか、タスク実行時にエラーがあった場合に呼び出されます。
		 * Event.COMPLETEをdispatchします。
		 **/
		internal function endTaskChain():void{
			//実行終了後に削除
			dispatchEvent(new Event("complete"));			
			delete TaskChain.chainMap[this];
		}
	}
}